package com.hgsoft.pubsv.component.apifilter.filter;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartResolver;

import com.hgsoft.pubsv.component.apifilter.SecurityParam;
import com.hgsoft.pubsv.component.apifilter.enums.ApiParamAttribute;
import com.hgsoft.pubsv.component.apifilter.enums.AuthParamAttribute;
import com.hgsoft.pubsv.component.apifilter.enums.EncryptTypeEnum;
import com.hgsoft.pubsv.component.apifilter.init.ApiSecuritData;
import com.hgsoft.pubsv.component.apifilter.propertie.SecurityPropertie;
import com.hgsoft.pubsv.component.apifilter.util.AESUtil;
import com.hgsoft.pubsv.component.apifilter.util.IPUtil;
import com.hgsoft.pubsv.component.apifilter.util.RSAUtils;
import com.hgsoft.pubsv.component.apifilter.util.SetUtil;
import com.hgsoft.pubsv.component.base.Constants;
import com.hgsoft.pubsv.component.base.RegexpContants;
import com.hgsoft.pubsv.component.base.context.ApiReqContextHolder;
import com.hgsoft.pubsv.component.base.dto.BaseApiReqDto;
import com.hgsoft.pubsv.component.base.dto.BaseApiRespDto;
import com.hgsoft.pubsv.component.base.dto.OAuthToken;
import com.hgsoft.pubsv.component.base.execption.ApiCodeException;
import com.hgsoft.pubsv.component.base.redis.RedisScriptUtil;
import com.hgsoft.pubsv.component.base.token.IOAuthCacheTokenService;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class EncryptionFilter implements Filter {

	/**
	 * 设置上传文件总量的最大值（所有上传文件）5M
	 */
	public static final long DEFAULT_SIZE_MAX = 5 * 1024 * 1024;

	/**
	 * 设置上传文件的最大值（单个文件) 1M
	 */
	public static final int DEFAULT_FILE_SIZE_MAX = 1 * 1024 * 1024;

	private SecurityPropertie securityPropertie;
	private MultipartResolver multipartResolver;
	private RedisScriptUtil redisScriptUtil;
	private IOAuthCacheTokenService authCacheTokenService;

	public EncryptionFilter(SecurityPropertie securityPropertie, RedisScriptUtil redisScriptUtil,
			MultipartResolver multipartResolver, IOAuthCacheTokenService authCacheTokenService) {
		this.securityPropertie = securityPropertie;
		this.redisScriptUtil = redisScriptUtil;
		this.multipartResolver = multipartResolver;
		this.authCacheTokenService = authCacheTokenService;
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {

	}

	/**
	 * 解决multipart/form-data中HttpServletRequest.getParameter无法获取参数问题
	 * 改为用MultipartFile可解决此问题
	 *
	 * @param request
	 * @return
	 */
	private HttpServletRequest getHttpServletRequest(ServletRequest request) {
		String contentType = request.getContentType();
		// log.info("contentType:{}", contentType);
		if (StringUtils.isNotBlank(contentType) && contentType.contains("multipart/form-data")) {
			// 返回 MultipartHttpServletRequest 用于获取 multipart/form-data 方式提交的请求中 上传的参数
			log.info("multipart:{}", contentType);
			return multipartResolver.resolveMultipart((HttpServletRequest) request);
		}
		return (HttpServletRequest) request;
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest req = getHttpServletRequest(request);
		HttpServletResponse resp = (HttpServletResponse) response;
		String uri = req.getRequestURI();
		log.debug("RequestURI: {}", uri);
		SecurityParam securityParam = getSecurityConfig(uri, req.getMethod());
		if (securityPropertie.getIsDebug()||securityParam.notFilter()) {
			log.debug("no securityParameter url:{}", uri);
			chain.doFilter(request, response);
			return;
		}

		String accessNo = req.getParameter(ApiParamAttribute.ACCESSNO.val());
		String encryptType = req.getParameter(ApiParamAttribute.ENCRYTYPE.val());
		String format = req.getParameter(ApiParamAttribute.FORMAT.val());
		String charset = req.getParameter(ApiParamAttribute.CHARSET.val());
		String timestamp = req.getParameter(ApiParamAttribute.TIMESTAMP.val());
		String version = req.getParameter(ApiParamAttribute.VERSION.val());
		// 采用RSAWITHAES加解密方式必传
		String encryptAesKey = req.getParameter(ApiParamAttribute.ENCRYAESKEY.val());

		@SuppressWarnings("rawtypes")
		BaseApiReqDto apiReqDto = new BaseApiReqDto<>();
		apiReqDto.setAccessNo(accessNo).setEncryptType(encryptType).setFormat(format).setVersion(version)
				.setEncryAesKey(encryptAesKey);

		try {
			// ip限流
			if (securityParam.ipLimit()) {
				String key = IPUtil.getIpAddr(req) + uri.hashCode();
				// 获取限制时间范围
				int limitPeriod = securityParam.ipLimitPeriod();
				// 获取限制访问次数
				int limitCount = securityParam.ipLimitCount();
				String luaScript = RedisScriptUtil.LIMIT_LUS_SCRIPT;
				Number count = redisScriptUtil.executeScript(Number.class, key, luaScript, limitCount + "",
						limitPeriod + "");
				if (count != null && count.intValue() <= limitCount) {
				} else {
					log.error("警告@uri:{},访问过于频繁，ip:{}已超限ip", uri, key);
					throw new ApiCodeException(Constants.FREQUENTLY_SUBMIT, "访问过于频繁，ip已超限:" + key);
				}
			}

			if (StringUtils.isBlank(accessNo) || StringUtils.isBlank(encryptType) || StringUtils.isBlank(format)
					|| StringUtils.isBlank(charset) || StringUtils.isBlank(timestamp) || StringUtils.isBlank(version)) {
				log.error("参数缺失accessNo:{},encryptType:{},format:{},charset:{},charset:{},timestamp:{},version:{}",
						accessNo, encryptType, format, charset, timestamp, version);
				throw new ApiCodeException(Constants.PARAM_NULL, Constants.PARAM_NULL_NAME);
			}

			String allowIPRange = securityPropertie.getAuthIpsByAccessNo(accessNo);
			String remoteAddr = IPUtil.getIpAddr(req);
			if (!IPUtil.checkIp(remoteAddr, allowIPRange)) {
				log.error("ip:{} 非法访问限制,请排查", remoteAddr);
				throw new ApiCodeException(Constants.URL_NOT_ALLOW, Constants.URL_NOT_ALLOW_NAME);
			}

			EncryptionResponseWrapper responseWrapper = new EncryptionResponseWrapper(resp);
			EncryptionReqestWrapper requestWrapper = null;
			if (securityParam.inDecode()) {
				requestWrapper = new EncryptionReqestWrapper(req);
				String requestData = requestWrapper.getRequestData();
				log.debug("RequestData: {}", requestData);
				if (!StringUtils.endsWithIgnoreCase(req.getMethod(), RequestMethod.GET.name())) {
					String decryptRequestData = decrypt(accessNo, encryptType, encryptAesKey, charset, requestData,
							securityParam);
					log.debug("DecryptRequestData: {}", decryptRequestData);
					requestWrapper.setRequestData(decryptRequestData);
				}
				// url参数解密
				Map<String, Object> paramMap = new HashMap<>();
				Enumeration<String> parameterNames = req.getParameterNames();
				String[] ignoreParameter = securityParam.ignoreParameter().split(":");
				List<String> ignoreKeys = ignoreParameter == null ? null : Arrays.asList(ignoreParameter);
				while (parameterNames.hasMoreElements()) {
					String paramName = parameterNames.nextElement();
					if (ApiParamAttribute.BIZCONTENT.val().equals(paramName)) {
						String paramValue = req.getParameter(paramName);
						String decryptParamValue = decrypt(accessNo, encryptType, encryptAesKey, charset, paramValue,
								securityParam);
						Map<String, Object> json = new HashMap<>();
						if (StringUtils.isNotBlank(decryptParamValue)) {
							json = JSONUtil.parseObj(decryptParamValue);
						}
						// 如果是选装发行渠道接入 不需要校验
						if (securityParam.needCheckTime()) {
							Long time = (Long) json.get(AuthParamAttribute.TIME.val());
							if (time == null) {
								throw new ApiCodeException(Constants.PARAM_NULL, "参数[time]缺失");
							}
							// 当前时间减去15分钟作为开始检验时间
							long start = Date.from(Instant.now().plus(-15, ChronoUnit.MINUTES)).getTime();
							// 当前时间加上15分钟作为结束检验时间
							long end = Date.from(Instant.now().plus(15, ChronoUnit.MINUTES)).getTime();
							// 客户端上送时间
							long lt = new Long(time);
							// 检验客户端请求报文的有效期（允许误差15分钟）
							if (lt > end || lt < start) {
								throw new ApiCodeException(Constants.ERR_PARAMETER_INVALID,
										Constants.ERR_PARAMETER_INVALID_NAME);
							}
						}
						// 是否需要校验token，如果是第三方发行渠道接入 不需要校验
						if (securityParam.needCheckToken()
								&& !SetUtil.isInSets(accessNo, securityPropertie.getNotTokenAccessNo())) {
							String clientKey = (String) json.get(AuthParamAttribute.CLIENTKEY.val());
							String tokenId = (String) json.get(AuthParamAttribute.TOKENID.val());
							if (StringUtils.isBlank(clientKey) || StringUtils.isBlank(tokenId)) {
								throw new ApiCodeException(Constants.ERR_TOKEN_INVALID,
										Constants.ERR_TOKEN_INVALID_NAME);
							}
							OAuthToken oAuthToken = authCacheTokenService.getAppUserToken(clientKey, accessNo);
							if (!tokenId.equals(oAuthToken.getToken())) {
								throw new ApiCodeException(Constants.ERR_TOKEN_INVALID,
										Constants.ERR_TOKEN_INVALID_NAME);
							}
							// 校验是否限制访问业务url
							if (StringUtils.isNotBlank(oAuthToken.getBusinessType())) {
								String limitUrl = securityPropertie
										.getTokenAuthUrlByBusinessType(oAuthToken.getBusinessType());
								log.debug("限制访问业务权限处理,oAuthToken:{}", oAuthToken);
								if (!SetUtil.isInSets(uri, limitUrl)) {
									log.error("非法访问业务oAuthToken:{},uri:{},limitUrl:{}", oAuthToken, uri, limitUrl);
									throw new ApiCodeException(Constants.URL_NOT_ALLOW, Constants.URL_NOT_ALLOW_NAME);
								}
							}
							apiReqDto.setClientKey(clientKey);
						}
						// 复写
						paramMap.putAll(json);
					} else if (!ApiParamAttribute.isExist(paramName) && !ignoreKeys.contains(paramName)) {
						String paramValue = req.getParameter(paramName);
						String decryptParamValue = decrypt(accessNo, encryptType, encryptAesKey, charset, paramValue,
								securityParam);
						paramMap.put(paramName, decryptParamValue);
					} else if (!ApiParamAttribute.isExist(paramName) && ignoreKeys.contains(paramName)) {
						String paramValue = req.getParameter(paramName);
						paramMap.put(paramName, paramValue);
					}
				}
				// 返回accessNo
				paramMap.put(ApiParamAttribute.ACCESSNO.val(), accessNo);
				checkXss(paramMap, ignoreKeys);
				requestWrapper.setParamMap(paramMap);
				log.debug("after decrypt request paramMap:{}", paramMap);
			}

			// 请求上下文
			ApiReqContextHolder.setBaseApiReqDto(apiReqDto);

			// 业务处理
			chain.doFilter(securityParam.inDecode() ? requestWrapper : req,
					securityParam.outEncode() ? responseWrapper : response);

			// 返回处理
			writeEncryptContent(responseWrapper, response, securityParam, accessNo, encryptType, charset);
		} catch (Exception e) {
			log.error("拦截器处理异常", e);
			handleException(e, response, securityParam, charset);
			return;
		} finally {
			// 请求上下文
			ApiReqContextHolder.resetBaseApiReqDto();
		}
	}

	/**
	 * 解密参数
	 *
	 * @param encryptType
	 * @param paramValue
	 * @return
	 * @throws Exception
	 */
	private String decrypt(String accessNo, String encryptType, String encryAesKey, String charset, String paramValue,
			SecurityParam securityParam) throws Exception {
		if (StringUtils.isNotBlank(securityParam.encryptType())
				&& !encryptType.equalsIgnoreCase(securityParam.encryptType())) {
			throw new ApiCodeException(Constants.ENCRYPTION_NOT_SUPPORT, Constants.ENCRYPTION_NOT_SUPPORT_NAME);
		}
		// 空值不加密
		if (StringUtils.isBlank(paramValue)) {
			return "";
		}
		if (EncryptTypeEnum.AES.val().equals(encryptType)) {
			String aesKey = securityPropertie.getAesKeyByAccessNo(accessNo);
			if (StringUtils.isBlank(aesKey)) {
				throw new ApiCodeException(Constants.CHANNEL_ENCRYPTION_CONF_ERR,
						Constants.CHANNEL_ENCRYPTION_CONF_ERR_NAME);
			}
			return AESUtil.decrypt(paramValue, aesKey, charset);
		} else if (EncryptTypeEnum.RSA.val().equals(encryptType)) {
			// 解密私钥
			String privateKey = securityPropertie.getRsaPrivateKeyByAccessNo(accessNo);
			if (StringUtils.isBlank(privateKey)) {
				throw new ApiCodeException(Constants.CHANNEL_ENCRYPTION_CONF_ERR,
						Constants.CHANNEL_ENCRYPTION_CONF_ERR_NAME);
			}
			return RSAUtils.decrypt(paramValue, privateKey);
		} else if (EncryptTypeEnum.RSAWITHAES.val().equals(encryptType)) {
			// 解密私钥
			String privateKey = securityPropertie.getRsaPrivateKeyByAccessNo(accessNo);
			if (StringUtils.isBlank(privateKey)) {
				throw new ApiCodeException(Constants.CHANNEL_ENCRYPTION_CONF_ERR,
						Constants.CHANNEL_ENCRYPTION_CONF_ERR_NAME);
			}
			String aesKey = RSAUtils.decrypt(encryAesKey, privateKey);
			log.debug("RSAWITHAES after decrypt aesKey:{}", aesKey);
			if (StringUtils.isBlank(aesKey)) {
				throw new ApiCodeException(Constants.ENCRYPTION_NOT_SUPPORT, Constants.ENCRYPTION_NOT_SUPPORT_NAME);
			}
			return AESUtil.decrypt(paramValue, aesKey, charset);
		} else {
			throw new ApiCodeException(Constants.ENCRYPTION_NOT_SUPPORT, Constants.ENCRYPTION_NOT_SUPPORT_NAME);
		}
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void writeEncryptContent(EncryptionResponseWrapper responseWrapper, ServletResponse response,
			SecurityParam securityParam, String accessNo, String encryptType, String charset) throws IOException {
		ServletOutputStream out = null;
		try {
			// 没配置加密
			if (!securityParam.outEncode()) {
				return;
			}
			String responseData = responseWrapper.getResponseData();
			log.debug("ResponseData: {}", responseData);
			BaseApiRespDto apiRespDto = JSONUtil.toBean(responseData, BaseApiRespDto.class);
			if (Constants.SUCCESS.equals(apiRespDto.getCode()) || StringUtils.isEmpty(apiRespDto.getCode())) {
				String json = JSONUtil.toJsonStr(apiRespDto.getBizContent());
				if (StringUtils.isNotBlank(json)) {
					apiRespDto.setFormat(JSONUtil.isJson(json) ? "JSON" : "String");
				}
				if (StringUtils.isNotBlank(securityParam.encryptType())
						&& !encryptType.equalsIgnoreCase(securityParam.encryptType())) {
					throw new ApiCodeException(Constants.ENCRYPTION_NOT_SUPPORT, Constants.ENCRYPTION_NOT_SUPPORT_NAME);
				}
				String bizContent = "";
				// 空值不处理
				if (StringUtils.isNotBlank(json)) {
					if (EncryptTypeEnum.AES.val().equals(encryptType)) {
						String aesKey = securityPropertie.getAesKeyByAccessNo(accessNo);
						if (StringUtils.isBlank(aesKey)) {
							throw new ApiCodeException(Constants.CHANNEL_ENCRYPTION_CONF_ERR,
									Constants.CHANNEL_ENCRYPTION_CONF_ERR_NAME);
						}
						bizContent = AESUtil.encrypt(json, aesKey, charset);
					} else if (EncryptTypeEnum.RSA.val().equals(encryptType)) {
						// 加密公钥
						String publicKey = securityPropertie.getRsaPublicKeyByAccessNo(accessNo);
						if (StringUtils.isBlank(publicKey)) {
							throw new ApiCodeException(Constants.CHANNEL_ENCRYPTION_CONF_ERR,
									Constants.CHANNEL_ENCRYPTION_CONF_ERR_NAME);
						}
						bizContent = RSAUtils.encrypt(json, publicKey);
					} else if (EncryptTypeEnum.RSAWITHAES.val().equals(encryptType)) {
						// 加密公钥
						String publicKey = securityPropertie.getRsaPublicKeyByAccessNo(accessNo);
						if (StringUtils.isBlank(publicKey)) {
							throw new ApiCodeException(Constants.CHANNEL_ENCRYPTION_CONF_ERR,
									Constants.CHANNEL_ENCRYPTION_CONF_ERR_NAME);
						}
						// 随机生成16位数字
						String aesKey = RandomUtil.randomStringUpper(16);
						log.debug("RSAWITHAES before encrypt aesKey:{}", aesKey);
						String encryptAesKey = RSAUtils.encrypt(aesKey, publicKey);
						bizContent = AESUtil.encrypt(json, aesKey, charset);
						apiRespDto.setEncryAesKey(encryptAesKey);
					} else {
						throw new ApiCodeException(Constants.ENCRYPTION_NOT_SUPPORT,
								Constants.ENCRYPTION_NOT_SUPPORT_NAME);
					}
				}
				apiRespDto.setBizContent(bizContent);
			}
			apiRespDto.setReceiveTime(DateUtil.now());
			responseData = JSONUtil.toJsonStr(apiRespDto);
			byte[] res = responseData.getBytes(charset);
			response.setContentLength(res.length);
			response.setCharacterEncoding(charset);
			out = response.getOutputStream();
			out.write(res);
		} catch (Exception e) {
			log.error("响应数据加密失败", e);
			throw new RuntimeException(e);
		} finally {
			if (out != null) {
				out.flush();
				out.close();
			}
		}
	}

	@SuppressWarnings({ "rawtypes" })
	private void handleException(Exception e, ServletResponse response, SecurityParam securityParam, String charset)
			throws IOException {
		ServletOutputStream out = null;
		try {
			BaseApiRespDto apiRespDto = handler(e);
			apiRespDto.setReceiveTime(DateUtil.now());
			String responseData = JSONUtil.toJsonStr(apiRespDto);
			byte[] res = responseData.getBytes(StringUtils.isNotBlank(charset) ? charset : "utf-8");
			response.setContentLength(res.length);
			response.setCharacterEncoding(charset);
			out = response.getOutputStream();
			out.write(res);
		} catch (Exception e1) {
			log.error("响应数据加密失败", e1);
			throw new RuntimeException(e);
		} finally {
			if (out != null) {
				out.flush();
				out.close();
			}
		}
	}

	private SecurityParam getSecurityConfig(String uri, String methodType) {
		uri = uri.endsWith("/") ? uri = uri.substring(0, uri.length() - 1) : uri;
		String prefixUri = methodType.toLowerCase() + ":" + uri;
		SecurityParam securityParam = ApiSecuritData.requestSecurityParamMap.get(prefixUri);
		if (securityParam == null) {
			log.debug("RequestURI default securityParameter");
			securityParam = new SecurityParam() {

				@Override
				public Class<? extends Annotation> annotationType() {
					return null;
				}

				@Override
				public String value() {
					return "";
				}

				@Override
				public boolean outEncode() {
					return securityPropertie.getOutEncode();
				}

				@Override
				public boolean needCheckToken() {
					return securityPropertie.getNeedCheckToken();
				}

				@Override
				public boolean needCheckTime() {
					return securityPropertie.getNeedCheckTime();
				}

				@Override
				public int ipLimitPeriod() {
					return securityPropertie.getIpLimitPeriod();
				}

				@Override
				public int ipLimitCount() {
					return securityPropertie.getIpLimitCount();
				}

				@Override
				public boolean ipLimit() {
					return securityPropertie.getIpLimit();
				}

				@Override
				public boolean inDecode() {
					return securityPropertie.getInDecode();
				}

				@Override
				public String ignoreParameter() {
					return securityPropertie.getIgnoreParameter();
				}

				@Override
				public String encryptType() {
					return securityPropertie.getEncryptType();
				}

				@Override
				public boolean notFilter() {
					return securityPropertie.getIsDebug();
				}
			};
		}
		return securityParam;

	}

	@Override
	public void destroy() {

	}

	/**
	 * 检测非法字符
	 * 
	 * @param paramMap
	 * @param ignoreParameter
	 *            过滤字符串
	 */
	private static void checkXss(Map<String, Object> paramMap, List<String> ignoreKeys) {
		// 过滤非法字符
		Map<String, Object> xssMap = paramMap.entrySet().stream()
				.filter(map -> xssCharacterCheck(map.getKey(), map.getValue(), ignoreKeys))
				.collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()));
		if (xssMap == null || !xssMap.isEmpty()) {
			throw new ApiCodeException(Constants.PARAM_XSS_FAIL, "存在非法字符输入,key:" + xssMap.keySet());
		}
	}

	/**
	 * XSS字符检测
	 *
	 * @param value
	 * @return
	 */
	private static boolean xssCharacterCheck(String key, Object value, List<String> ignoreKeys) {
		if (value == null || !(value instanceof String) || (ignoreKeys != null && ignoreKeys.contains(key))) {
			return false;
		}
		Pattern scriptPattern = Pattern.compile(RegexpContants.xxsRegex,
				Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
		return scriptPattern.matcher((String) value).find();
	}

	/**
	 * 全局异常捕获
	 * 
	 * @param e
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	public static BaseApiRespDto handler(Exception e) {
		BaseApiRespDto BaseApiRespDto = null;
		if (e instanceof ApiCodeException) {
			log.error("业务异常：" + e.getMessage(), e);
			BaseApiRespDto = new BaseApiRespDto<>(((ApiCodeException) e).getCode(), ((ApiCodeException) e).getMsg());
		} else {
			log.error("未知异常：" + e.getMessage(), e);
			BaseApiRespDto = new BaseApiRespDto<>(Constants.SYS_ERR, Constants.SYS_ERR_NAME);
		}
		return BaseApiRespDto;
	}

}
